package com.gmail.at.vssabinin.train_transport_system_part1;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 
 * This class contains the logic of protocol of client-server exchange
 * the protocol is: first 4 bytes the length of the total message, the
 * next 4 bytes the length of the string which determines the logic to 
 * work with message, the next is string, and after string some 
 * serializable object, it's length could be calculated as total length
 * minus 8 bytes and minus length of the string.
 * 
 */
public class Protocol {
	
	/**
	 * 
	 * Class Message represents the message according to which Protocol
	 * works. It has 2 object: string - message type and object which
	 * it transport. 
	 * 
	 */
	public class Message {
		
		// message type
		private String type;
		
		// object which we transport
		private Object object;
	
		
		public Message(String t, Object obj) {
			this.setType(t);
			this.setObject(obj);
		}
		
		public Object getObject() {
			return object;
		}
		
		public void setObject(Object object) {
			this.object = object;
		}
		
		public String getType() {
			return type;
		}
		
		public void setType(String type) {
			this.type = type;
		}
		
	}
	
	private Logger logger = LoggerFactory.getLogger("Protocol");
	
	// method to make a 4 bytes array from an integer
	private byte[] integerToByteArray(int integer) {
		
		return ByteBuffer.allocate(4).putInt(integer).array();
		
	}
	
	/**
	 * Creates a new byte array from given object Message
	 * 
	 * @param mes Message to be encoded in byte[] array
	 * @return byte array which represent this message according to Protocol or
	 * 		   null if it's impossible to create a byte array
	 * 
	 */
	public byte[] createMessage(Message mes) {
		
		List<Byte> message = new ArrayList<Byte>();
		
		// the answer to be send
		byte[] answer = null;
		
		// detect the length of the string
		byte[] stringLength = this.integerToByteArray(mes.type.getBytes().length);
		
		ByteArrayOutputStream os = new ByteArrayOutputStream();
		
		ObjectOutputStream out = null;
		
		// create a new byte array from given object
		try {
			
			out = new ObjectOutputStream(os);
			out.writeObject(mes.object);
			byte[] byteObj = os.toByteArray();
			
			/* 
			 * total length is 4 bytes header 4 bytes string length and length 
			 * of string and object
			 */
			byte[] totalLength = this.integerToByteArray(byteObj.length + 
					mes.type.getBytes().length + 4 + 4);
			
			// adding total length
			for (int i = 0; i < 4; i++) {
				message.add(totalLength[i]);
			}
			
			// adding length of the string
			for (int i = 0; i < 4; i++) {
				message.add(stringLength[i]);
			}
			
			// adding string
			for (Byte b : mes.type.getBytes()) {
				message.add(b);
			}
			
			// adding object
			for (Byte b : byteObj) {
				message.add(b);
			}
			
			// create a new array of bytes and add the elements of list to it
			answer = new byte[message.size()];
			for(int i = 0; i < message.size(); i++) {
				answer[i] = message.get(i);
			}		
		} catch (IOException e) {
			this.logger.error("Error during creating a new message byte[] in " +
					"Protocol " + e.getLocalizedMessage());
		} finally {
			try {
				out.close();
			} catch (IOException e) {
				this.logger.error("Error in closing the stream " + 
						e.getLocalizedMessage());
			}
			try {
				os.close();
			} catch (IOException e) {
				this.logger.error("Error in closing the stream " + 
						e.getLocalizedMessage());
			}
		}
		this.logger.info("Creating a new message with type " + mes.getType());
		
		// return created byte array
		return answer;
	}
	
	/**
	 * It's known that 4 first bytes represent the whole length of message, so 
	 * the method gives the information about the length of the message. If it's 
	 * impossible to decode message return null
	 * 
	 * @param buffer received message from network
	 * @return the length of expected message or null, if it's impossible to decode
	 * 
	 */
	public Integer decodeMessageLength(ByteBuffer buffer) {
		
		/* check that it's possible to read as less 4 bytes from the given buffer
		 * if it is - create a new Integer representing the whole length.
		 */
		if (buffer.remaining() > 4) {
			
			byte[] totalLength = new byte[4];
			
			for(int i = 0; i < 4; i++) {
				totalLength[i] = buffer.array()[i];
			}
			
			return ByteBuffer.wrap(totalLength).getInt();
		}
		return null;
	}
	
	
	/**
	 * Check the integrity of the byte buffer
	 * 
	 * @param buffer the message to be checked
	 * @return true if the given buffer contains all the received message, false if 
	 *         it' not
	 * 
	 */
	public boolean isMessageComplete(ByteBuffer buffer) {
		if(buffer.limit() == this.decodeMessageLength(buffer)) {
			
			return true;
		}
		
		return false;
	}
	
	/**
	 * Check the buffer for integrity and decode it to the Message object according 
	 * to Protocol.
	 * 
	 * @param buffer a byte array to be decoded
	 * @return a new Message object if it's possible to decode message or null 
	 * 		   if it's not
	 * 
	 */
	public Message decodeMessage(ByteBuffer buffer) {
		
		// message to be created
		Message message = null;
		
		byte[] stringLengthByte = new byte[4];
		
		// getting length of string
		for(int i = 0; i < 4; i++) {
			stringLengthByte[i] = buffer.array()[i + 4];
		}
		
		ByteBuffer b = ByteBuffer.wrap(stringLengthByte);
		
		// the integer that represent's the length of the string
		Integer stringLengthInteger = b.getInt(); 
		
		// byte array that has the length of the string
		byte[] byteString = new byte[stringLengthInteger];
		
		// filling byte array with bytes of the string
		for (int i = 0; i < stringLengthInteger; i++) {
			byteString[i] = buffer.array()[i + 8];
		}
		
		// string of the Message type object
		String type =  new String(byteString, Charset.defaultCharset());
		
		// byte array that has the length of the object
		byte[] byteObject = new byte[this.decodeMessageLength(buffer) 
		                             - 8 - stringLengthInteger];
		
		// filling byte array with bytes of the object
		for (int i = 0; i < byteObject.length; i++) {
			byteObject[i] = buffer.array()[i + 8 + stringLengthInteger];
		}
		
		// creating a new object from the created array
		ByteArrayInputStream is = new ByteArrayInputStream(byteObject);
		ObjectInputStream input = null;
		try {
			
			input = new ObjectInputStream(is);
			message = new Message(type, input.readObject());
			
		} catch (IOException e) {
			
			this.logger.error("Error during decoding message " + 
					e.getLocalizedMessage());
			
		} catch (ClassNotFoundException e) {
			this.logger.error("Received object is not recognized " + 
					e.getLocalizedMessage());
			
		} finally {
			try {
				input.close();
			} catch (IOException e) {
				this.logger.error("Error in closing the stream " + 
						e.getLocalizedMessage());
			}
			try {
				is.close();
			} catch (IOException e) {
				this.logger.error("Error in closing the stream " + 
						e.getLocalizedMessage());
			}
		}
		
		// return created message
		return message; 	
	}
}